package com.tianji.promotion.service.impl;

import cn.hutool.core.bean.copier.CopyOptions;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.StringUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.constants.PromotionConstants;
import com.tianji.promotion.discount.Discount;
import com.tianji.promotion.discount.DiscountStrategy;
import com.tianji.promotion.domain.dto.CouponDiscountDTO;
import com.tianji.promotion.domain.dto.OrderCourseDTO;
import com.tianji.promotion.domain.dto.UserCouponDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.enums.CouponStatus;
import com.tianji.promotion.enums.ExchangeCodeStatus;
import com.tianji.promotion.enums.MyLockStrategy;
import com.tianji.promotion.enums.MyLockType;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import com.tianji.promotion.utils.CodeUtil;
import com.tianji.promotion.utils.MyLock;
import com.tianji.promotion.utils.PermuteUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;

/**
 * <p>
 * 用户领取优惠券的记录，是真正使用的优惠券信息 服务实现类
 * </p>
 *
 * @author root
 * @since 2023-11-24
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class UserCouponMqServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;
    private final IExchangeCodeService exchangeCodeService;
    private final StringRedisTemplate redisTemplate;
    private final RedissonClient redissonClient;
    private final RabbitMqHelper mqHelper;
    private final ICouponScopeService couponScopeService;

    //领取优惠券
    //分布式锁可以对优惠券加锁
    @MyLock(name = "lock:coupon:uid:#{id}")
    @Override
    public void receiveCoupon(Long id) {
        // 1. 根据id查询优惠券相关信息  做校验
        if(id == null){
            throw new BadRequestException("非法参数");
        }
//        Coupon coupon = couponMapper.selectById(id);
        // 从redis中获取优惠券信息
        Coupon coupon = queryCouponByCache(id);
        if(coupon == null){
            throw new BadRequestException("优惠券不存在");
        }
//        if(coupon.getStatus() != CouponStatus.ISSUING){
//            throw new BadRequestException("优惠券状态异常");
//        }
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
            throw new BadRequestException("优惠券未开始或已过期");
        }
//        if (coupon.getTotalNum() <= 0 || coupon.getIssueNum() >= coupon.getTotalNum()){
        if (coupon.getTotalNum()<=0){
            throw new BadRequestException("库存不足");
        }

        //获取当前用户对该优惠券的已领数量
//        Integer count = this.lambdaQuery()
//                .eq(UserCoupon::getCouponId, id)
//                .eq(UserCoupon::getUserId, userId)
//                .count();
//        if(count != null && count >= coupon.getUserLimit()){
//            throw new BadRequestException("已达领取限额");
//        }
//        // 2. 优惠券已发放数量+1
//        couponMapper.incrIssueNum(id);
//        // 3. 生成用户券
//        saveUserCoupon(userId,coupon);

        //Long类型 -128~127 之间是同一个对象  超过该区间是不同的对象
        //Long.toString() 底层是 new String() 所以还是不同的对象
        //Long.toString().intern()  intern() 方法是强制从常量池中取字符串
//        synchronized (userId.toString().intern()) {
//            // 从aop上下文中  获取当前类的代理对象
//            IUserCouponService userCouponServiceProxy = (IUserCouponService)AopContext.currentProxy();
//            userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null);//这种写法是调用代理对象的方法，该方法是有事务处理的
//        }
        //通过工具类实现分布式锁
//        String key = "lock:coupon:uid:" + userId;
//        RedisLock redisLock = new RedisLock(key,redisTemplate);
//        boolean isLock = redisLock.tryLock(5, TimeUnit.SECONDS);
//        if(!isLock){
//            throw new BizIllegalException("操作太频繁了");
//        }
//        try {
//            //从aop上下文中 获取当前类的处理对象
//            IUserCouponService userCouponServiceProxy = (IUserCouponService)AopContext.currentProxy();
//            userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null);//这种写法是调用代理对象的方法，该方法是有事务处理的
//
//        }finally {
//            // 释放锁
//            redisLock.unlock();
//        }
        //通过redisson实现分布式锁
//        String key = "lock:coupon:uid:" + userId;
//        RLock lock = redissonClient.getLock(key);
//
//        try {
//            boolean isLock = lock.tryLock();//指定过期时间看门狗机制会失效，使用默认的30s
//            if (!isLock){
//                throw new BizIllegalException("操作太频繁了");
//            }
//            //从aop上下文中 获取当前类的处理对象
//            IUserCouponService userCouponServiceProxy = (IUserCouponService)AopContext.currentProxy();
//            userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null);//这种写法是调用代理对象的方法，该方法是有事务处理的
//
//        }finally {
//            lock.unlock();
//        }
// 从aop上下文中 获取当前类的处理对象
////        IUserCouponService userCouponServiceProxy = (IUserCouponService)AopContext.currentProxy();
////        userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null);//这种写法是调用代理对象的方法，该方法是有事务处理的
        Long userId = UserContext.getUser();
        //统计已领取的数量
        String key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + id; //prs:user:coupon:优惠券id
        // increment 代表本次领取后的已领取数量
        Long increment = redisTemplate.opsForHash().increment(key, userId.toString(), 1);
        // 校验是否超过限领的数量
        if(increment>coupon.getUserLimit()){
            redisTemplate.opsForHash().increment(key, userId.toString(), -1);
            throw new BizIllegalException("超出领取数量");
        }
        //修改优惠券库存  -1
        String couponKey = PromotionConstants.COUPON_CACHE_KEY_PREFIX + id;
        redisTemplate.opsForHash().increment(couponKey,"totalNum",-1);

        // 发送消息到 mq  消息内容： userId  couponId
        UserCouponDTO msg = new UserCouponDTO();
        msg.setUserId(userId);
        msg.setCouponId(id);
        mqHelper.send(
                MqConstants.Exchange.PROMOTION_EXCHANGE,
                MqConstants.Key.COUPON_RECEIVE,
                msg);

    }

    /**
     * 从redis中获取优惠券的信息
     * @param id
     * @return
     */
    private Coupon queryCouponByCache(Long id) {
        // 1. 拼接key
        String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + id;
        // 2. 获取数据
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        Coupon coupon = BeanUtils.mapToBean(entries, Coupon.class, false, CopyOptions.create());
        return coupon;
    }

    //保存用户券
    private void saveUserCoupon(Long userId, Coupon coupon) {
        UserCoupon userCoupon = new UserCoupon();
        userCoupon.setUserId(userId);
        userCoupon.setCouponId(coupon.getId());
        LocalDateTime termBeginTime = coupon.getTermBeginTime();// 优惠券使用开始时间
        LocalDateTime termEndTime = coupon.getTermEndTime();// 优惠券使用截止时间
        //这两个时间只可能同时为null 或者 都不是null,
        // 如果同时为null,那么优惠券的有效期这个字段就不可能是null,优惠券的使用结束时间就是 当前时间 + 优惠券的有效期（天）
        if (termBeginTime == null){
            termBeginTime = LocalDateTime.now();
            termEndTime = termBeginTime.plusDays(coupon.getTermDays());
        }
        userCoupon.setTermBeginTime(termBeginTime);
        userCoupon.setTermEndTime(termEndTime);

        // 保存
        this.save(userCoupon);
    }

    @Override
    @Transactional
    public void exchangeCoupon(String code) {
        // 1. 校验code是否为空
        if(StringUtils.isBlank(code)){
            throw new BadRequestException("非法参数");
        }
        // 2. 解析兑换码得到自增id
        long serialNum = CodeUtil.parseCode(code);//自增id
        // 3. 判断兑换码是否已兑换   采用redis的bitmap结构 setbit  key  offset  1   如果方法返回 true代表兑换码已兑换
        boolean result = exchangeCodeService.updateExchangeCodeMark(serialNum,true);
        if(result){
            // 说明兑换码已经被兑换了
            throw new BizIllegalException("兑换码已被使用");
        }
        try {
            // 4. 判断兑换码是否存在 根据自增id 主键查询
            ExchangeCode exchangeCode = exchangeCodeService.getById(serialNum);
            if(exchangeCode == null){
                throw new BizIllegalException("兑换码不存在");

            }
            // 5. 判断是否过期
            LocalDateTime now = LocalDateTime.now();
            LocalDateTime expiredTime = exchangeCode.getExpiredTime();
            if(now.isAfter(expiredTime)){
                throw new BizIllegalException("兑换码已过期");
            }

            // 6. 校验并生成用户券
            Long userId = UserContext.getUser();
            // 查询优惠券信息
            Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
            if(coupon == null){
                throw new BizIllegalException("优惠券不存在");
            }
            checkAndCreateUserCoupon(userId,coupon, serialNum);
            // 7. 更新兑换码状态
        }catch (Exception e){
            // 将兑换码状态重置
            exchangeCodeService.updateExchangeCodeMark(serialNum,false);
            throw e;
        }

    }

    @Override
    @Transactional
    @MyLock(name = "lock:coupon:uid:n #{userId}",
            waitTime = 1,
            leaseTime = 5,
            lockType = MyLockType.RE_ENTRANT_LOCK,
            lockStrategy = MyLockStrategy.FAIL_FAST)
    public void checkAndCreateUserCoupon(Long userId, Coupon coupon, Long serialNum) {

//        synchronized (userId.toString().intern()){
            //获取当前用户对该优惠券的已领数量
            Integer count = this.lambdaQuery()
                    .eq(UserCoupon::getCouponId, coupon.getId())
                    .eq(UserCoupon::getUserId, userId)
                    .count();
            if(count != null && count >= coupon.getUserLimit()){
                throw new BadRequestException("已达领取限额");
            }
            // 2. 优惠券已发放数量+1
            int r = couponMapper.incrIssueNum(coupon.getId());
            if(r == 0){
                throw new BizIllegalException("优惠券库存不足");
            }
        // 3. 生成用户券
            saveUserCoupon(userId,coupon);

            if(serialNum != null){
                // 修改兑换码的状态
                exchangeCodeService.lambdaUpdate()
                        .eq(ExchangeCode::getId,serialNum)
                        .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                        .set(ExchangeCode::getUserId,userId)
                        .update();
            }
//        }
    }

    @Transactional
    @Override
    public void checkAndCreateUserCouponNew(UserCouponDTO msg) {
//        Integer count = this.lambdaQuery()
//                .eq(UserCoupon::getCouponId, coupon.getId())
//                .eq(UserCoupon::getUserId, userId)
//                .count();
//        if(count != null && count >= coupon.getUserLimit()){
//            throw new BadRequestException("已达领取限额");
//        }

        // 1. 从db中查询用户券信息
        Coupon coupon = couponMapper.selectById(msg.getCouponId());
        if(coupon == null){
            return;
        }
        // 2. 优惠券已发放数量+1
        if(couponMapper.incrIssueNum(coupon.getId()) == 0){
            return;
        }
        // 3. 生成用户券
        saveUserCoupon(msg.getUserId(),coupon);

    }

    @Override
    public List<CouponDiscountDTO> findDiscountSolution(List<OrderCourseDTO> courses) {
        // 1. 查询当前用户可用的优惠券   coupon 和 user_coupon  条件: userID  status=1  查什么: 优惠券的规则 优惠券id 用户券id
        // select c.id,c.discount_type,c.specific,c.threshold_amount,c.discount_value,c.max_discount_amount,uc.id
        //             from coupon c inner join user_coupon uc on c.id = uc.coupon_id
        //             where uc.user_id = xxx and uc.status = 1
        List<Coupon> coupons = getBaseMapper().queryMyCoupon(UserContext.getUser());
        if(CollUtils.isEmpty(coupons)){
            return CollUtils.emptyList();
        }
        log.debug("用户的优惠券共有 {}张", coupons.size());
        // 2. 初筛
        // 2.1 计算订单总金额  对courses的price累加
        int totalAmount = courses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
        log.debug("订单总金额 {}",totalAmount);
        // 2.2 校验优惠券是否可用
        List<Coupon> availableCoupons = coupons.stream().filter(coupon -> DiscountStrategy.getDiscount(coupon.getDiscountType()).canUse(totalAmount, coupon)).collect(Collectors.toList());
        if(CollUtils.isEmpty(availableCoupons)){
            return CollUtils.emptyList();
        }
        log.debug("经过初筛后，还有 {}张", availableCoupons.size());
        // 3. 细筛（需要考虑优惠券的限定范围）排列组合
        Map<Coupon,List<OrderCourseDTO>> avaMap = findAvailableCoupons(availableCoupons,courses);
        if (avaMap.isEmpty()){
            return CollUtils.emptyList();
        }
        availableCoupons = new ArrayList<>(avaMap.keySet());
        log.debug("经过细筛后的优惠券有 {}张",avaMap.size());
        for(Coupon coupon : availableCoupons){
            log.debug("优惠券: {},  {}",
                    DiscountStrategy.getDiscount(coupon.getDiscountType()).getRule(coupon),coupon);
        }
        // 排列组合
        List<List<Coupon>> solutions = PermuteUtil.permute(availableCoupons);
        for (Coupon availableCoupon : availableCoupons){
            solutions.add(List.of(availableCoupon));//添加单券
        }
        log.debug("排列组合");
        for (List<Coupon> solution : solutions){
            List<Long> cids = solution.stream().map(Coupon::getId).collect(Collectors.toList());
            log.debug("{}",cids);
        }

        // 4. 计算每一种组合的优惠明细
//        List<CouponDiscountDTO> dtos = new ArrayList<>();
//        for (List<Coupon> solution : solutions){
//            CouponDiscountDTO dto = calculateSolutionDiscount(avaMap,courses,solution);
//            dtos.add(dto);
//        }
        // 5. 使用多线程改造第4步  并行计算每一种组合的优惠明细
        log.debug("多线程计算 每一种组合的优惠明细");
        List<CouponDiscountDTO> dtos = Collections.synchronizedList(new ArrayList<>(solutions.size()));//线程安全集合
        CountDownLatch latch = new CountDownLatch(solutions.size());
        for(List<Coupon> solution : solutions){
            CompletableFuture.supplyAsync(new Supplier<CouponDiscountDTO>() {
                @Override
                public CouponDiscountDTO get() {
                    CouponDiscountDTO dto = calculateSolutionDiscount(avaMap,courses,solution);
                    return dto;
                }
            }).thenAccept(new Consumer<CouponDiscountDTO>() {
                @Override
                public void accept(CouponDiscountDTO dto) {
                    log.debug("方案最终优惠 {}  方案中优惠券使用了 {}  规则{}",dto.getDiscountAmount(),dto.getIds(),dto.getRules());
                    dtos.add(dto);
                    latch.countDown();//计数器-1
                }
            });
        }
        try {
            latch.await(2, TimeUnit.SECONDS);//最多等两秒
        }catch (InterruptedException e){
            log.error("多线程计算组合优惠明细 报错了",e);
        }
        // 6. 筛选最优解CouponDiscountDTO






        return findBestSolution(dtos);
    }

    private List<CouponDiscountDTO> findBestSolution(List<CouponDiscountDTO> solutions) {
        // 1. 创建两个map 分别记录用券数量相同，优惠金额最高  和  优惠金额相同，用券最少
        Map<String,CouponDiscountDTO> moreDiscountMap = new HashMap<>();
        Map<Integer,CouponDiscountDTO> lessCouponMap = new HashMap<>();
        // 2. 循环方案 向map中记录
        for(CouponDiscountDTO solution : solutions){
            // 2.1 对优惠券id 升序，转字符串，然后用逗号拼接
            String ids = solution.getIds().stream()
                    .sorted(Comparator.comparing(Long::longValue))
                    .map(String::valueOf)
                    .collect(Collectors.joining(","));
            CouponDiscountDTO old = moreDiscountMap.get(ids);
            // 旧方案金额大于当前
            if(old != null && old.getDiscountAmount() >= solution.getDiscountAmount()){
                // 抛弃此方案
                continue;
            }

            old = lessCouponMap.get(solution.getDiscountAmount());
            int newSize = solution.getIds().size();
            // 旧方案用券数量小于当前
            if(old != null && newSize > 1 && old.getIds().size() <= newSize){
                // 抛弃此方案
                continue;
            }
            // 2.4 添加到map
            moreDiscountMap.put(ids,solution);
            lessCouponMap.put(solution.getDiscountAmount(),solution);
        }
        // 3. 求两个map交集
        Collection<CouponDiscountDTO> bestSolution = CollUtils.intersection(moreDiscountMap.values(), lessCouponMap.values());
        // 4. 按优惠金额倒序
        List<CouponDiscountDTO> latestBestSolution = bestSolution.stream()
                .sorted(Comparator.comparing(CouponDiscountDTO::getDiscountAmount).reversed())
                .collect(Collectors.toList());
        return latestBestSolution;

    }

    /**
     * 计算每一个方案的优惠明细
     * @param avaMap  优惠券和可用课程的映射集合
     * @param courses  订单中的所有课程
     * @param solution  方案
     * @return
     */
    private CouponDiscountDTO calculateSolutionDiscount(Map<Coupon, List<OrderCourseDTO>> avaMap, List<OrderCourseDTO> courses, List<Coupon> solution) {
        // 1. 创建方案结果dto对象
        CouponDiscountDTO dto = new CouponDiscountDTO();
        // 2. 初始化商品id和商品折扣明细的映射，  初始折扣明细全都设置为0,设置map结果 key:商品id;value:0
        Map<Long, Integer> detailMap = courses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, orderCourseDTO -> 0));
        // 3. 循环该方案的优惠信息
        // 3.1 循环方案中优惠券
        for(Coupon coupon : solution){
            // 3.2 取出该优惠券对应的可用课程
            List<OrderCourseDTO> availableCourses = avaMap.get(coupon);
            // 3.3 计算可用课程的总金额（商品价格 - 该商品的折扣明细）
            int totalAmount = availableCourses.stream().mapToInt(value -> value.getPrice() - detailMap.get(value.getId())).sum();
            // 3.4 判断优惠券是否可用
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            if(!discount.canUse(totalAmount,coupon)){
                continue;//该优惠券不可用，继续处理下一个优惠券
            }
            // 3.5 计算该优惠券使用后的折扣值
            int discountAmount = discount.calculateDiscount(totalAmount, coupon);
            // 3.6 计算商品的折扣明细，更新到detailMap
            calculateDetailDiscount(detailMap,availableCourses,totalAmount,discountAmount);
            // 3.7 累加累加每一个优惠券的优惠金额，赋值给dto对象
            dto.getIds().add(coupon.getId());
            dto.getRules().add(discount.getRule(coupon));
            dto.setDiscountAmount(discountAmount + dto.getDiscountAmount());//这里应该是所有生效优惠券累加的结果
        }
        
        return dto;
    }

    /**
     * 计算商品的折扣明细，更新到detailMap
     * @param detailMap 商品id 和 商品的优惠明细
     * @param availableCourses  可使用该优惠券的课程
     * @param totalAmount  可用的课程的总金额
     * @param discountAmount  当前优惠券能优惠的金额
     */
    private void calculateDetailDiscount(Map<Long, Integer> detailMap, List<OrderCourseDTO> availableCourses, int totalAmount, int discountAmount) {
        //规则：前面的商品按比例计算，最后一个商品折扣明细 = 总的优惠金额 - 前面商品的优惠金额
        int times = 0;//代表已处理的商品个数
        int remainDiscount = discountAmount;//代表剩余的优惠金额
        for(OrderCourseDTO availableCourse : availableCourses){
            times++;
            int discount = 0;
            if(times == availableCourses.size()){
                // 说明是最后一个课程
                discount = remainDiscount;
            }else {
                // 前面的课程  按比例
                discount = availableCourse.getPrice() * discountAmount / totalAmount;//先乘再除，否则可能为0
                remainDiscount -= discount;
            }
            // 将商品的折扣明细添加到 detailMap  累加
            detailMap.put(availableCourse.getId(),discount + detailMap.get(availableCourse.getId()));
        }
    }

    /**
     * 细筛，查询每一个优惠券对应的可用课程
     * @param coupons 初筛之后的优惠券集合
     * @param orderCourses  订单中的课程集合
     * @return
     */
    private Map<Coupon, List<OrderCourseDTO>> findAvailableCoupons(List<Coupon> coupons, List<OrderCourseDTO> orderCourses) {
        Map<Coupon, List<OrderCourseDTO>> map = new HashMap<>();
        // 1. 循环遍历初筛后的优惠券集合
        for(Coupon coupon : coupons){
            // 2. 找出每一个优惠券的可用集合
            // 2.1 判断优惠券是否限定了范围
            List<OrderCourseDTO> availableCourse = orderCourses;
            if(coupon.getSpecific()){
                // 2.2 查询限定范围
                List<CouponScope> scopeList = couponScopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();
                // 2.3 得到限定范围的id集合
                List<Long> scopeIds = scopeList.stream().map(CouponScope::getBizId).collect(Collectors.toList());
                // 2.4 从 orderCourses 订单中所有课程集合  筛选该范围内的课程
                availableCourse = orderCourses.stream()
                        .filter(orderCourseDTO -> scopeIds.contains(orderCourseDTO.getCateId()))
                        .collect(Collectors.toList());
            }
            if (CollUtils.isEmpty(availableCourse)){
                continue;
            }
            // 3. 计算该优惠券可用课程的金额
            int totalAmount = availableCourse.stream().mapToInt(OrderCourseDTO::getPrice).sum();
            // 4. 判断该优惠券是否可用
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            if (discount.canUse(totalAmount,coupon)){
                map.put(coupon,availableCourse);
            }
        }

        return map;
    }

}
